feat(acp): enforce deterministic agent git commit identity - #6177
feat(acp): enforce deterministic agent git commit identity#6177wpfleger96 wants to merge 52 commits into
Conversation
Agent sessions could commit under the human operator's ambient git identity: only buzz-dev-mcp's shim applied the nostr author/signing GIT_CONFIG_* env, and only to its own shell-tool children. The native shells of claude-code, codex, and goose never saw it, so a bare `git commit` there resolved to whatever the repo/global config carried — erasing the AI-attribution signal (e.g. block/buzz #3140). Make the identity machine-managed across every harness: - New `buzz-git-identity` crate holds the pure author/email/signing/ keyfile logic as the single source of truth, consumed by both the shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. - A `git` enforcement wrapper (installed on PATH ahead of the real binary) scrubs GIT_AUTHOR_*/GIT_COMMITTER_* from the child env, rejects `-c user.*`, `--config-env=user.*`, `--author`, and `--reset-author`, and on push refuses any outgoing commit not authored by the agent identity, then execs real git. - The harness lifts the identity + NIP-GS signing config onto the agent-runtime child and installs the wrapper plus the nostr signer/credential helpers via buzz-acp's own multicall, so native shells of all runtimes inherit both. Composed over the desktop's per-URL credential helper; skipped when no nostr key is present. - Prompt guidance (base_prompt.md, nest_agents.md) updated: identity is machine-managed; credit the operator via Co-authored-by/ Signed-off-by trailers, never user.name/email/-c/--author. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Round-1 review fixes on the deterministic agent-commit-identity work: - L3 push gate resolved the effective command through git aliases (config-defined and inline -c alias.x=push) so a push disguised as a custom alias can no longer skip outgoing-author verification. - Verification subprocesses now carry repository context (-C, --git-dir, --work-tree, --namespace); an outgoing tip that resolves to a real ref but whose range cannot be computed fails closed instead of being skipped as nothing-to-check. - AcpClient::shutdown deletes the git-identity keyfile tempdir explicitly via TempDir::close before the process-group kill. Relying on Drop right before std::process::exit leaked the 0600 nostr keyfile ~80% of runs; all client-owning error/timeout exits funnel through shutdown_and_exit, which takes the client by value. Checkpoint commit: the review round continues on this branch with the identity-authority and push-boundary rework. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The enforcement wrapper trusted the same caller-mutable GIT_CONFIG_* environment it was meant to constrain, so `env -u GIT_CONFIG_COUNT git commit` fell back to repo-local human identity and the push gate derived its expected identity from that same mutable config and failed open. - Authority: harness/shim write a 0600 identity manifest beside the keyfile; the wrapper locates its own install dir by PATH canonicalization, re-applies identity+signing GIT_CONFIG_* at the highest index before exec, and reads L3's expected author from the manifest. Manifest present = enforce; absent = passthrough. - L1b eligibility sources the key from BUZZ_PRIVATE_KEY then NOSTR_PRIVATE_KEY, decoupled from credential-helper discovery, and fails the managed session closed when identity cannot be installed. - Push gate uses git's own resolved plan (push --dry-run --porcelain --no-verify) instead of predicting argv, covering aliases, -C, --all/--mirror/--tags, config refspecs; unresolvable = fail closed. - Author preflight rejects commit -C/-c/--amend that would leave a non-agent author; rebase/cherry-pick of upstream history pass. - Signing-disable via -c/--no-gpg-sign rejected at argv. - Process-level mutation tests spawn the real multicall and go red when each enforcement layer is removed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Polish mobile timeline navigation (#5874) chore(release): release Buzz Desktop version 0.5.17 (#6234) fix(prompt): simplify pickup follow-through (#6186) fix(mcp): scope todo usage (#6216) fix(desktop): bound remote agent mention authorization (#6224) fix: bump h2 for RUSTSEC-2026-0258 (#6222) fix(desktop): bind presence retry timers (#6213) ci: make file-size policy a first-class gate (#6187) fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198) chore(release): release Buzz Desktop version 0.5.16 (#6191) fix(desktop): restore release agent mentions (#6182) test(desktop): cover exact workflow batch limit (#6168) chore(release): release Buzz Desktop version 0.5.15 (#6173) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: feat(managed-agents): close five Claude Code agent-config gaps (#4557) chore(hooks): keep mobile analysis out of pre-commit (#6236) fix(shared-ui): delay hover disclosures by default (#5821) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Address the reconciled Thufir/Gurney round-2 findings on the deterministic agent git-identity gate: - Injected identity args now outrank caller `-c`/GIT_CONFIG_PARAMETERS/ include.path/repo config, and a tampered manifest fails closed (C1). - Shell (`!`) push aliases are treated as opaque and rejected before any probe, so an alias cannot transmit before verification (C2). - Author-preserving commit modes gain a patch-id exemption so legitimate rebases of upstream human commits pass while new human-authored commits are still refused (I3). - BUZZ_PRIVATE_KEY outranks NOSTR_PRIVATE_KEY at both layers and the canonical key is restaged unconditionally as the child NOSTR_PRIVATE_KEY, so the harness and dev-mcp shim can never install split identities (I5). - The push predictor's subprocess probe is bounded by a hard timeout and fails closed on expiry (I6). Adds a real-binary integration test driving the buzz-acp spawn path so the install_git_identity wiring is regression-covered, plus mutation-sensitive unit tests for the key precedence and timeout paths. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
An ordinary (non-shell) git alias whose body carries global `-c`/`--config-env` config is expanded by git in-process after the wrapper's injected identity/signing `-c` options, so the alias-added config outranks the authority and could silently re-author or unsign a commit. enforce() inspects only the literal argv and never the alias body, so it could not catch this — a repo-local alias recreated the original human-attribution leak through the managed wrapper. Add a pre-exec verify_alias_safety pass that resolves the effective alias chain and refuses any alias whose expansion introduces global configuration, per the favor-rejection principle rather than modeling git's full alias grammar. Plain-subcommand aliases keep working. Shell (`!`) aliases stay out of scope here: their git invocations re-enter the wrapper on PATH and push-bearing ones are already rejected as opaque. Also gate the unconditional `nostr::ToBech32` import in git_identity_enforcement.rs behind #[cfg(unix)] (its only use is a unix-only test) so the Windows clippy gate stops failing on unused-import under -D warnings. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…shell aliases The prior blocklist scanned an alias body for known-bad config tokens, but git's quote-aware alias parser dequotes `'-c' 'user.email=…'` into a live config channel that a naive whitespace scan never sees — a parser-parity bypass. And `!` shell aliases were treated as safe on the commit path on the false premise that their inner git re-enters this wrapper; git prepends its own exec-path to PATH, so the inner git is the real binary and its -c outranks the inherited env authority, committing as an arbitrary human, unsigned. Invert verify_alias_safety to an allowlist: a non-shell alias is admitted only when every body token is a trivially-safe bare word (no quote/backslash, no -c/--config-env in any spelling, no =-valued option); anything else is refused without modeling git's grammar. Reject all shell aliases outright in a managed session, commit path included. This makes the whole config-injection class end by construction. Gurney's certified shapes (ci=commit, st=status, lg=log --oneline, pub=push origin main) stay allowed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311) fix(desktop): morph the drawer panel icon instead of sliding it (#6306) feat(desktop): refine repository-aware project workspaces (#6003) Fix mobile Activity thread navigation (#5850) perf(desktop): parallelize relay agent directory rebuild (#6258) Refine the mobile emoji picker (#5853) fix(desktop): exclude archived agents from nest, order regeneration (#5905) Add font size and conversation density preferences (#5644) fix(desktop): emit camelCase config-write payload fields (#6062) fix(desktop): downscale large avatars for agent-share PNG body (#6260) fix(desktop): preserve early relay auth challenges (#3320) Polish mobile message actions (#5873) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
… policy A bare-word alias whose body carried identity/signing flags (`commit --author … --no-gpg-sign`) passed the allowlist because every token is a plain bare word, and enforce()/verify_commit_author() keyed on the literal typed subcommand (the alias name, never the expanded `commit`) — so the flag preflights never fired. A repo-local alias through the managed wrapper could author as a human and disable signing. verify_alias_safety now returns the alias's fully-resolved expansion (typed globals + recursively-expanded command with accumulated body tokens and the user's trailing argv). run() holds that expansion to the same enforce() and verify_commit_author() preflight as a directly-typed command, keyed on the expanded subcommand. An alias can no longer do more than its expansion could typed directly, so there is no alias-specific flag list to maintain. Shell-alias and unclassifiable-syntax refusals are unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(buzz-acp): loosen workspace-scan guardrail to allow named paths (#6261) fix(buzz-dev-mcp): expand leading ~ in read_file/str_replace paths (#6271) perf(desktop): move five hot renderer paths from JS into Rust (#6024) fix(media): accept portrait video resolutions (#6058) fix(desktop): hide archived channels from #/Tab autocomplete (#6156) Unify mobile channel details (#6113) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Git continues resolving aliases after the wrapper reaches its bounded expansion limit. Refuse when the next command word remains an alias so a partial expansion cannot bypass managed identity and signing policy. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tity The deterministic-agent-commit-identity enforcement is unconditional: an agent always commits under its nostr identity. Sovereignty (VISION_SOVEREIGN.md) says the operator, not the platform, decides whose identity their agent's commits carry on their own machine. Add BUZZ_GIT_IDENTITY=agent|user (default agent, settable per-agent via persona env). `user` reuses the existing review-hardened unconfigured-session path — no git wrapper on PATH, no manifest, no injected authorship/signing config — so vanilla git resolves the operator's own identity. The nostr credential helper (relay git-over-HTTP auth) keeps installing in user mode: auth is not attribution. The mode is read once at spawn by the harness and shim, never by the wrapper per-invocation, so an agent cannot disable enforcement mid-session. An unrecognized value fails the spawn loudly rather than silently choosing a mode. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: (40 commits) chore(release): release Buzz Desktop version 0.5.18 (#6489) fix(desktop): simplify duplicate agent provenance (#6401) test(benchmarks): expand Buzz-native dataset (#6448) fix(desktop): sender names in notifications + macOS click-through routing (#6427) docs: clarify two-layer moderation ownership (#6481) Fix mobile thread tail and iOS channel header (#6399) chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392) polish(desktop): finish Projects navigation and context chrome (#6429) fix(desktop): clarify add agents channel action (#6374) Repair stale large channel roster snapshots (#6251) feat(desktop-messages): show compact Buzz link metadata (#6252) feat(workflows): reply in-thread from send_message action (#6178) perf(desktop): split discover_acp_providers into cheap and forced paths (#6330) fix(desktop): restore recent channel sorting (#6402) fix(desktop): isolate main timeline stacking context from focus drawer (#6398) fix(desktop): make reconnect repair lossless (#6415) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lper In BUZZ_GIT_IDENTITY=user mode the harness returned Ok(None) before resolving the agent key, so a headless BUZZ_PRIVATE_KEY-only launch left the dev-mcp shim's credential-helper-only branch with no key — the toggle silently removed relay git AUTH, not just attribution. auth != attribution must hold on every launch path, and an operator who set user mode on a configured session has not unconfigured their auth. Resolve the canonical key (same BUZZ_PRIVATE_KEY-over-NOSTR_PRIVATE_KEY, child-over-process precedence) and stage it as the child's NOSTR_PRIVATE_KEY BEFORE the user-mode early return. user mode still installs no wrapper, manifest, keyfile, or injected config on the harness side; no key at all is still Ok(None). Wrapper unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The extra_env merge in AcpClient::spawn only staged a persona value when the parent process env lacked that key, so a global BUZZ_GIT_IDENTITY silently defeated every per-agent override in both directions — the child-over-process lookup in install_git_identity never saw the dropped value. Treat the var as an operator-controlled exception and always stage the persona value; the general parent-wins semantics for other keys are unchanged. Also derive Debug on Shim so the shim seam test's expect_err compiles under clippy -D warnings (--workspace --all-targets). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(desktop): restore human barge-in over agent TTS in huddles (#6431) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The test mutated process-global env across .await points and unconditionally removed the variable, clobbering any caller-supplied value instead of restoring it. Add an RAII guard that captures the prior value and restores-or-removes on drop (including on assertion panic). It is the only env-mutating spawn test in buzz-acp, so no cross-test serialization is required. Both-direction real-merge-loop assertions are unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
I found three blocking contract failures at c64e62e599ffaaf223a111c34ea600d341967f57:
-
P1: managed git can create and push unsigned agent-authored commits.
enforce()rejects--no-gpg-signforcommit|am|tag|rebase|cherry-pick|revert, but notmerge/pull; the outgoing gate then checks only author email and never verifies signature presence or the expected key. A focused real-wrapper reproduction created an unsigned merge withgit merge --no-gpg-sign --no-ff, observed%G? = N, and pushed it successfully.git commit-treeproduces the same systemic bypass through the installed wrapper. This contradicts the PR’s “every commit is automatically signed” contract. At minimum cover every signing-capable porcelain path; the robust boundary is to require a valid expected-key NIP-GS signature on each new agent-authored outgoing commit, with merge/plumbing push regressions. -
P1:
BUZZ_GIT_IDENTITY=usercan erase relay git authentication through descriptor env. Desktop stages its credential-helperGIT_CONFIG_*entries and then writesdescriptor.envafterward (runtime.rs:781-811). A descriptor withBUZZ_GIT_IDENTITY=userplusGIT_CONFIG_COUNT=0reaches the harness; user mode intentionally returns before installing identity config, leaving the clobbered count in place. That violates the explicit auth-is-not-attribution guarantee. Preserve/reapply the helper after descriptor env or reserve the completeGIT_CONFIG_*family, and test the actual Desktop-to-harness layering rather than a freshCommand. -
P2: the changed Nest guidance is not delivered to existing installs and conflicts with #6707. This PR rewrites
nest_agents.mdbut leavesNEST_AGENTS_VERSIONat 4 and adds no content/upgrade test, so existing v4 Nests never receive it. #6707 bumps 4→5, tests refresh/preservation, and intentionally removes the unconditional humanSigned-off-bymandate that #6177 retains. The two branches edit the same block and #6707’s content test rejects #6177’s exact wording. Reconcile onto one policy contract; keep #6707’s version bump and upgrade coverage rather than landing both as-is.
The runtime mechanism in #6177 is broader than #6707: #6707 is policy-neutral generated guidance and does not replace identity enforcement. Its wording already allows a managed agent identity while keeping repository policy authoritative, so it is the safer documentation base to combine with a corrected runtime implementation.
Current CI for #6177 is green and git diff --check passes, but neither covers these boundary failures.
A user-supplied GIT_CONFIG_* entry (persona/agent/global env_vars) is layered onto the spawn command AFTER the relay credential-helper GIT_CONFIG_* Buzz stages, so a single GIT_CONFIG_COUNT=0 silently orphans the helper. In BUZZ_GIT_IDENTITY=user mode install_git_identity returns before re-staging anything, so the clobber stands and relay git auth is erased — violating the auth-is-not-attribution guarantee. Reserve the whole GIT_CONFIG* family (bare name + GIT_CONFIG_ prefix) in the shared is_reserved_env_key predicate. This is the single chokepoint every env layer already routes through (save-time validation, spawn-time filter, and the remote-deploy launch env), closing both the local and headless paths at once. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: (54 commits) Extract community persistence (#6668) Fix mobile Huddle agent voice turn states (#6611) Add inline profile camera capture (#6680) Hide Huddles in mobile agent DMs (#6676) fix(desktop): polish inline chip states (#6718) Centralize replaceable event persistence (#6660) feat(workflows): discover trigger filter values (#6712) feat(desktop): simplify the message action rail (#6529) fix(desktop): restore icon-only remote marker (#6491) fix(ci): prevent poisoned Rust caches (#6618) docs(security): route reports through private advisories (#6728) fix(composer): wrap Buzz chip labels without orphaning icons (#6581) fix(desktop): bound thread /query and surface load errors, not false-empty (#6447) fix(messages): route edits to the owning composer (#6575) fix(mobile): join starter channels after accepting invite (#5915) Add mobile profile editing (#6583) fix(desktop): align jump-to-latest pill with composer height (#6606) fix(desktop): emit singular `mention` feed category so alerts route correctly (#6665) fix(mobile): recover stale and shuffled messages (#6691) feat(mobile): browse and join open channels (#6243) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - replace the generated Nest's unconditional human author/sign-off rules with portable guidance that separates authorship, material co-authorship, DCO certification, and cryptographic signing - defer attribution to repository-local policy, forbid inferred or guessed identities, and require inspection of every outgoing commit - bump the Nest template version so existing installations refresh, with regression coverage for fresh generation and upgrade preservation ### Related issue None found. Related runtime identity work exists in #6177, but this PR is intentionally limited to the generated Nest guidance and its refresh behavior. ### Testing - `bin/just desktop-tauri-clippy` - `bin/just desktop-tauri-test` - `bin/just file-size-check` - `git diff --check` - pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`, and `desktop-tauri-checks` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
…c-agent-commit-identity Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…sh gate Unsigned agent-authored commits created via git merge/pull --no-gpg-sign or the commit-tree plumbing sail past the flag-based enforce() rejection, which only covers commit/am/tag/rebase/cherry-pick/revert on the literal argv. Extend the existing verify_push walk — which already visits every outgoing commit not on a remote and checks author email — to also require a valid NIP-GS signature by the agent key on each agent-authored commit, gated on the session actually enforcing signing (commit.gpgSign=true). One check then covers every commit-creation path, including plumbing, without growing the enforce() blocklist. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(cli): preserve signatures in event reads (#6884) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
ff6a3e4 to
d157e94
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
P1: caller-supplied config can introduce an alias that bypasses both alias safety and push verification. verify_alias_safety only recognizes exact lowercase inline -c alias.* definitions, then probes config using repository-context arguments only; is_push_command follows the same incomplete view (crates/buzz-git-identity/src/git_wrapper.rs:370-417,458-520). But enforce permits config keys outside its protected identity/signing list (:588-605,666-678). Therefore git -c include.path=/tmp/evil x, where the include defines alias.x = !git -c user.email=human … commit --no-gpg-sign, is treated as a real non-push command and handed to Git. Git then resolves the newly introduced shell alias, whose inner git bypasses the wrapper exactly as the threat model at :447-451 describes. --config-env=alias.x=ENV and case-varied -c ALIAS.x=… take the same path; an injected alias resolving to push also skips outgoing author/signature verification.
Resolve aliases under the caller’s complete effective config without executing them, or fail closed on every config channel that can introduce aliases/includes. Add commit and push regressions for include.path, --config-env aliases, and case variants. The earlier keyless and headless-helper lifecycle blockers are fixed at this head, but this reopens the unsigned-commit/push trust-boundary failure.
Review was read-only against exact head ba05511a843c8635b5e300f2ff33e61ed219e00f; I did not check out or execute PR code. Exact-head standard CI is green.
…smuggling The alias-safety and push-verification probes read `git config --get alias.<name>` under repo-context globals only, so an alias introduced through the caller's own config channels was invisible to them: `git -c include.path=<f> x` (file defines `alias.x = !git … commit`), `--config-env=alias.x=VAR`, and case-varied `-c ALIAS.x` all classified as real non-alias commands and reached the real git, which resolved the alias and bypassed both the shell-alias refusal and outgoing-author verification. Resolve every probe under the caller's complete config-injecting globals (`-c`/`--config-env`, attached and split forms) via `alias_probe_ctx`, so the probe sees exactly the alias set git will expand. Include/config-env/case variants now hit the existing `!`-refusal and bare-word allowlist with no new policy. This makes the hand-rolled `inline_aliases` map redundant (git's own resolution gives correct inline-over-file precedence and case-insensitive section names), so it is removed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The alias-safety and push-detection probes resolved `git config --get alias.<name>` under an allowlist of context globals (repo-context in round 4, plus the config-injection channels in round 6). A caller global outside that allowlist that changes which config git reads was therefore invisible to the probe while the real invocation still applied it. `--bare` is the concrete gap: it changes repository discovery, so `git -C <dir> --bare x` can expand a shell/push alias defined only in the bare view — one the probe, resolving the non-bare view, never sees — bypassing the shell-alias refusal and the outgoing author/signature gate. Replace the allowlist with the complete caller global set: the probe context is now every global token before the subcommand, so each probe resolves under the exact repository and configuration git will use. This closes the class rather than the instance — any global that would corrupt a probe corrupts the real invocation identically, so probe and real git share one view and fail closed together. The invariant is documented at the definition so it is not re-narrowed. The context also now feeds the push-verification probes (rev-list/show), which had the same dropped-`--bare` divergence from the push being verified. Authority identity/signing `-c` entries still splice last, so caller config cannot outrank them. Removes the redundant `repo_context_args`/`config_probe_globals`/ `alias_probe_ctx` split; adds real-wrapper commit + push regressions for a `--bare`-selected alias. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
split_globals is the single point of truth for where the subcommand begins, and every alias/push probe resolves under the globals it extracts. git 2.54 treats --shallow-file as a separate-value global, but the table omitted it: git -C <repo> --shallow-file -c x makes real git consume -c as the shallow-file path and dispatch alias x, while the wrapper ate x as the value of -c, found no subcommand, and skipped every preflight — reopening the shell-alias and push-signature bypass. Add --shallow-file to VALUE_LONG_OPTS with a version-pinned comment, a grammar test pinning every git 2.54 separate-value global, and real-wrapper shell/push regressions using the option-like-value shape. Also repair 12 alias unit tests that regressed silently at the round-6 refactor: probe-context derivation moved from inside the probes to the run caller, but these tests still passed ctx=vec![] with their alias config inline in argv, so they probed under an empty context. Build ctx via caller_globals(argv), matching production. This crate is absent from the CI test-unit -p list, so the regression never surfaced in CI. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Two blocking defects remain at exact head e8cb5dab193521e8e8888508b1c1a3c75edf2426:
-
P1: a forged local remote-tracking ref bypasses all outgoing author and signature checks.
rev_list_outgoingcomputes candidates asrev-list <source> --not --remotes, butrefs/remotes/*is caller-writable local state, not evidence that a commit exists at the push destination. An agent can create an arbitrary wrong-author or unsigned commit, rungit update-ref refs/remotes/forged/main HEAD, and then push it. The dry-run plan still names the pushed source, but the rev-list is empty, so both verification loops accept without inspecting HEAD. Derive exclusions from commits positively established at the actual destination, fail closed when that set cannot be established, and add a real-wrapper regression for the forged-ref case. -
P1: agent-mode ACP overwrites Desktop’s staged per-relay credential config. Desktop stages
GIT_CONFIG_COUNT=2and indices 0–1 directly on the childCommand.install_git_identitydetects that helper and declines to add another, butto_git_config_envchooses its append index only from the harness process environment. In the normal Desktop-to-harness path that base is zero, so agent identity entries replace the child command’s count and indices 0–1, orphaningcredential.<relay>/git.helperand breaking relay Git authentication. Compose against the actual child-command environment (or explicitly preserve/reapply those entries) and add an agent-mode Desktop layering regression; the current Desktop-helper test covers only the early-returning user mode.
The latest alias/config fixes otherwise address the prior include/config-env/case-variant and --shallow-file parser gaps, but these two defects still violate the core deterministic identity and authenticated-push contract.
… config on the child The push gate excluded commits reachable from refs/remotes/*, which is caller-writable: an agent could update-ref a forged remote-tracking ref to HEAD and hide an unsigned or wrong-author commit from both verification loops. Read the destination git resolved on the porcelain To line and derive exclusions from its real object ids via ls-remote, failing closed when the destination or the remote read is unavailable. Agent-mode ACP composed its GIT_CONFIG_* append index from the harness process env, but Desktop stages the per-relay credential helper directly on the child command. Compose against the count the child will actually see (child-staged, else inherited) so the helper survives instead of being overwritten at index 0. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: feat(desktop): add team sharing to community catalog (#3995) Refresh mobile utility surfaces and theme picker (#6944) fix(desktop): complete project empty and context states (#6980) Fix mobile jump-to-latest flicker (#6807) refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777) refactor(db): split channel membership store (#6782) feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776) fix(desktop): resolve exact typed mentions on space (#6862) perf(desktop): restore project context during startup (#6939) fix(desktop): lift right auxiliary pane above shared header backdrop (#6966) fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962) feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
One blocking trust-boundary defect remains at exact head c402144dd1dc215758071c131d684f2c970c5322:
P1: a caller-controlled receive-pack redirects the real push away from the remote that supplies the exclusion set. resolve_push_sources records the porcelain To <dest> label as the destination (lines 955-985), then remote_object_ids runs a separate git ls-remote <dest> and trusts those object IDs as proof that commits are already present (lines 1249-1271). But git push supports --receive-pack / --exec, plus remote.<name>.receivepack, and the To label does not encode that override. The dry-run and real push therefore use the custom receive-pack while the exclusion probe does not.
I reproduced this with vanilla Git and no PR code: create a wrong-author commit; seed a decoy bare repo with that SHA; leave a second bare repo empty; configure a remote whose URL is the decoy and whose remote.<name>.receivepack helper executes git-receive-pack against the empty repo; then push the commit through that remote. Porcelain reports To <decoy>, ls-remote <decoy> returns the bad commit, and rev-list HEAD --not <that SHA> is empty. The real push invokes the configured receive-pack and successfully writes the unchecked commit to the second repo. The same bypass is available through the documented --receive-pack / --exec flags.
Reject custom receive-pack configuration and flags in managed mode, or otherwise make the exclusion proof use the exact effective push transport and endpoint. Add a real-wrapper regression where the advertised To repository already contains the offending SHA but the receive-pack target does not.
The previous forged refs/remotes/* and Desktop credential-composition blockers are fixed at this head. The remaining lifecycle, alias/config grammar, and ordinary push-plan paths reviewed cleanly, but this redirect still breaks the core guarantee that every outgoing commit is checked against the actual destination.
…n-sensitivity - Fix verify_push_rejects_multiple_pushurls: add both A and B as explicit pushurls (remote set-url --add --push twice); a single --add only gives one push destination since git ignores remote.origin.url once any pushurl exists. - Fix verify_push_rejects_insteadof_rewrite_replay: use pushInsteadOf+insteadOf chain so To header names A (push dest) while ls-remote --get-url A returns B (insteadOf rewrite) and B holds HEAD. Guard catches A≠B and refuses. Plain insteadOf rewrites both push and fetch so To already shows B, defeating the test. Seed B with HEAD for mutation-sensitivity: without verify_destination_stable ls-remote A→B returns HEAD's IDs, exempting it, and A receives the commit. - Fix integration tests (wrapper_refuses_receive_pack_flag_and_leaves_target_empty, wrapper_refuses_alias_with_abbreviated_receive_pack_flag): correct orientation to origin→decoy (seeded with HEAD, supplies exclusion set) + script exec's git-receive-pack $actual (empty write target). Previous setup had origin→actual and script→url_repo, so actual stayed empty regardless of the guard. Verified: without the guard, actual/refs/heads/main appears after git push --receive-pack. - Fix remote.origin.receivepack config test 1b: set config value to script path (not 'git-receive-pack url_repo'). Origin still points at decoy. All 92 unit tests and 14 integration tests pass. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary Finish the remaining database-store extraction tracked by [TheSentinel454#2](TheSentinel454#2) in one reviewable PR. This consolidates the previously stacked domain slices after #6782 merged. It preserves the runtime/store boundary established by #6660, #6668, #6700, and #6782 while separating database runtime infrastructure from domain-owned persistence: - `runtime/` owns pool construction and sizing, writer/reader routing, read sessions and route proofs, transaction infrastructure, observability primitives, replica fencing, health support, migrations, and cross-cutting runtime tests. - `store/` owns domain records, SQL, row parsing, locks and invariants, `Db` domain methods, focused tests, and logical-operation datastore spans. - `lib.rs` remains a 57-line compatibility facade that preserves existing crate-root paths and `Db` method signatures through re-exports. Domain coverage includes API tokens, authentication allowlists, reminders, event queries, threads, reactions, feeds, users and DMs, push, workflows/runs/approvals, relay membership and invites, product feedback, moderation/admin moderation, relay admin actions/operators, git repositories, archived identities, usage, partition maintenance, deletion, channel membership inherited from merged #6782, and the final runtime/store layout. The branch has been rebased onto current `main`. Database changes that landed there were incorporated rather than overwritten: `relay_admin_actions.rs` and `relay_operators.rs` now live under `store/`, their 27 public `Db` wrappers and existing behavior remain intact, and every wrapper has exactly one fixed-name datastore span. Concurrent changes to migration, moderation, admin moderation, and error handling are also retained. ### Exact base and head - Base: `main` at `ed11c8d8bf0a17402be5cf243724f89471530d2f` - Head: `codex/issue-2-store-extraction` at `be24430472d1a87ac5c0d6026c620cd6caea3537` ### Related issue - Structural tracker: [TheSentinel454#2](TheSentinel454#2) - Domain trackers: [#6](TheSentinel454#6), [#7](TheSentinel454#7), [#12](TheSentinel454#12), [#13](TheSentinel454#13) - Acceptance trackers: [#17](TheSentinel454#17), [#19](TheSentinel454#19) This supersedes #6783, #6784, #6787, #6788, #6789, #6792, #6820, #6794, #6796, #6797, #6798, #6799, #6804, #6805, #6806, #6808, #6809, #6811, #6812, #6813, #6814, #6815, and #6890. Their discussions remain available for review history. ### #17 / #19 acceptance - Preserves the metric names, fixed labels, transaction/lock timing boundaries, and privacy/cardinality constraints introduced by #6700. - Keeps exactly one datastore span per public logical operation, including the 27 relay-admin wrappers added on `main`. - Removes `store_ownership.rs`; physical ownership and focused source guards now enforce the boundary directly. - Leaves no `impl Db`, domain SQL, focused domain test group, or datastore span in `lib.rs`. - Preserves existing public paths such as `buzz_db::channel`, `buzz_db::event`, and `buzz_db::workflow` through crate-root re-exports while keeping internal `runtime` and `store` namespaces private. ### Non-goals - No SQL, schema, locking, transaction, retry, timeout, or client-visible behavior changes. - No generic store traits, domain handles, broad `PgExecutor` migration, new store crate, raw pool accessor, or broader directory reorganization. - No tracker issues are closed by this PR. ### Risk The cumulative diff is large but structural. Risk is primarily module-path, ownership, or conflict-resolution drift. It is mitigated by preserving public re-exports, comparing the newly moved `main` implementations to their upstream source, source guards, touched-crate compilation, PostgreSQL-backed test coverage, and an independent exact-head review on a separate clean Blox workstation. ### Testing Author workstation `buzz-tornquist-pr-6987-rebase`, rebased branch ending at exact head `be24430472d1a87ac5c0d6026c620cd6caea3537`: - `cargo fmt --all --check` - `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` - `cargo test -p buzz-db --lib` — 113 passed, 240 PostgreSQL tests intentionally ignored - `cargo test -p buzz-db --test observability_source` — 2 passed - PostgreSQL-backed `buzz-db` coverage under native PostgreSQL — 235 passed in the shared serial run; the five shared-state/config-sensitive cases passed as isolated reruns against fresh schemas, including the two owner-limit tests with their fixture's `BUZZ_MAX_COMMUNITIES_PER_OWNER=3` - `cargo test -p buzz-relay --lib -- --test-threads=1` under native PostgreSQL/Redis — 991 passed; the three current-month partition-sensitive identity-archive cases passed after provisioning the August 2026 test partition; 87 infrastructure-marked tests remained ignored - Source/diff guards — relay-admin implementation bodies match current `main`; all 27 public wrapper signatures are retained; exactly one datastore span wraps each wrapper; `lib.rs` has zero `impl Db` blocks and zero datastore spans; no duplicate top-level relay-admin modules or `store_ownership.rs`; `error.rs` matches current `main` Independent clean review workstation `buzz-tornquist-pr-6987-review`, detached at exact head `be24430472d1a87ac5c0d6026c620cd6caea3537`: - `cargo fmt --all --check` - `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` - `cargo test -p buzz-db --lib` — 113 passed, 240 ignored - `cargo test -p buzz-db --test observability_source` — 2 passed - Exact-head ownership/re-export/instrumentation audit — no remaining actionable findings --------- Signed-off-by: OpenAI Codex <codex@openai.com> Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: OpenAI Codex <codex@openai.com>
This PR implements MVP, iOS-only, [NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant push notifications. A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any message that appears in the in-app Notifications tab. ## Enrollment flow The first time the client first connects to a relay with `BUZZ_PUSH_ENABLED`: ```mermaid sequenceDiagram autonumber participant App as Buzz iOS app participant iOS participant Relay as Buzz relay participant Attest as Apple App Attest participant Gateway as Push gateway App->>Relay: Fetch NIP-11 push capability Relay-->>App: Push profile, current relay public key, and limits par App->>iOS: Request notification permission iOS-->>App: Permission result and App->>iOS: Register for remote notifications iOS-->>App: Device token end App->>Gateway: Request installation challenge Gateway-->>App: Single-use challenge App->>Attest: Attest installation transcript Attest-->>App: Attestation proof App->>Gateway: Enroll device token and proof Gateway-->>App: Installation handle App->>Gateway: Request delegation challenge Gateway-->>App: Single-use challenge App->>Attest: Assert relay-key delegation Attest-->>App: Assertion App->>Gateway: Create delegation Gateway-->>App: Opaque endpoint grant App->>Relay: Publish encrypted push lease and filters Relay-->>App: Lease acknowledged ``` ## Push-time flow When a notification-eligible event is received by the relay: ```mermaid %%{init: { "sequence": { "actorMargin": 20, "width": 110, "messageMargin": 18, "diagramMarginX": 8, "wrap": true } }}%% sequenceDiagram autonumber participant Relay as Buzz relay participant Gateway as Push gateway participant APNs as Apple Push<br/>Notification service participant iOS participant NSE as Notification service<br/>extension Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload APNs-->>Gateway: 200 OK: request accepted Gateway-->>Relay: 200 OK: accepted status APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1 iOS->>NSE: Invoke extension<br/>original notification content NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003 NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target ``` relay → push gateway → APNs -> NSE -> Notification Center ## Known limitations The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design. The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once. This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation. ## Validation Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini: - A second real Buzz client published a uniquely marked message through the hosted relay. - The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no `simctl push` or simulated notification was used. - The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body. - After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name. - Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message. Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step. ## Independent pre-reviews - **First pass:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585) found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by [retry-safe bootstrap](12c66ea62) and [authenticated renewal plus a cross-replica quota](8e5ece0bd). [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962) found delegation generation burning and an edited applied migration, resolved by [exact-generation revocation](c26d2159d) and a [forward-only migration](956c1d099). [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1) found no blockers. - **Exact-head re-review:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68), [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849), and [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb) independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com>
Pinky is opening this PR on Wes’s behalf. ## Summary Reduce two separately measured mobile delays without changing the relay API or removing rich message rendering: - Publish the finite channel-list snapshot without waiting for live subscription setup. - Batch active channel-list subscriptions into sorted, deterministic chunks of at most 128 explicit channel IDs, retaining unchanged chunks. - Install replacement chunks before retiring old coverage. Retain old chunks across thrown replacement failures; filter callbacks to the current relay/identity and still-desired channels; clean up retired/in-flight work across disconnect and disposal. - Scope the custom-emoji Markdown matcher to known shortcodes actually referenced in the rendered content, rather than embedding the whole community palette in every message’s regex. Preserve unknown literals, shared-colon token boundaries, event-tag URL priority, content edits, and code literals. - Honor explicit zero retry hints without inventing a ten-second session-wide gate, while preserving the ordinary live-subscription retry backoff and any already-active gate. ## Matched performance results Medians of three before and three after process-cold launches, alternated on the same authenticated iPhone 17 Pro / iOS 26.5 simulator. Before is mobile source at `e76c81968b65b0755b83efdd59dc3375c59ddf40`; after is this production patch before two documentation-only comment fixes. First channel-list frame: 11.617s → 3.179s · 73% lower latency Live setup duration: 8.475s → 0.185s · 98% lower latency Channel-open first message-list frame: 2.754s → 1.230s · 55% lower latency Message data ready → first frame: 1.977s → 0.286s · 86% lower latency Channel-open reveal complete: 2.845s → 1.394s · 51% lower latency Channel-open data readiness: 0.770s → 0.944s · 23% higher latency The gain is client-side orchestration/rendering, not a claim that the relay became faster. First channel-list frame ranges were 10.835–11.788s before and 2.872–3.395s after; channel-open first-frame ranges were 1.560–2.906s before and 1.149–1.317s after. ### Measurement boundaries - Debug simulator builds, CPU sampling disabled, bounded timestamp probes enabled identically. These are not release/physical-device measurements. - Startup clock starts at Dart `main`; build/install/native pre-main time is excluded. Auth/preferences and OS/disk caches are retained between new processes. - Same account scale: 113 active channels. Latest-message events varied slightly with live activity (1543–1546). - Channel-open uses the same initial 50-row history window, 97 query events, and 67 provider events. The 2306-entry emoji palette is explicitly loaded before navigation on both sides; palette preparation is excluded from the channel-open clock and happens after the startup frame measurement. - Both diagnostic builds temporarily disabled unused avatar segmentation to work around the existing Google ML Kit arm64-simulator slice limitation. The workaround, dependency/native changes, auto-navigation, and all probes are excluded from this PR. ## Validation - Full mobile package suite: `flutter test` — 1890 passed. - `just mobile-check` — 506 files unchanged; analyzer clean. - `just file-size-check` — policy tests and all client ratchets passed. - `git diff --check` — passed. - New lifecycle regressions cover front-sorting insertion across a chunk boundary while replacement readiness is paused, failure retention/departed-channel filtering, retired generation + disconnect cleanup, disposal, chunk limits, unchanged-set reuse, and scope switches. - Emoji unit/widget coverage includes a 2500-unused-emoji palette, unknown tokens, case matching at the component level, shared-colon boundaries, rich text, event URL priority, and content edits. - Fresh-frame source review traced the subscription queue/fences, callback scopes, duplicate-event paths, matcher/wiring, and retry scheduling. - At committed/pushed head `13a83b628c8411c5885e6f76a250ba87accf6067`, all normal pre-push hooks passed: `mobile-checks` (formatter, analyzer, and the full 1890-test mobile suite), `file-size-check`, `branch-skew`, and `push-head-scope`. The commit hook formatted 506 files with no changes. Runtime measurements preceded only the two documentation-comment fixes; no runtime source changed afterward. ## Limits / follow-ups - `RelaySession.subscribe` still settles under its existing EOSE/fallback/retryable-CLOSED contract. “Setup completed” is not an unconditional EOSE or live-delivery guarantee. This PR does not add status-aware replacement ownership. - The channel-message provider still awaits subscribe before fetching history; that separate serialization is not removed here. - Oversized Huddle queries and the separate history batching path above 128 active channels remain follow-ups, as do pre-existing read-state initialization/size warnings. - Palette-only widget refresh and upstream Markdown uppercase-dispatch behavior are not changed. - A clean source build still has the existing Google ML Kit arm64-simulator issue; the profiling workaround is not a proposed product fix. Originating Buzz conversation: buzz://message?channel=793b0522-7995-4375-b1a6-fd94a96fa21d&id=6ba88afdec78ab2cfb6728afcd4a6d10f29e6aa33ff0f62f45d6750381e4d789 --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
…hell spawns (#6904) ## Why PR #6330 split agent harness/runtime detection into a cheap (cache-only) path and a forced (spawning) path. Two regressions followed, both surfacing as every harness showing "(not installed)" / "CLI missing" across the agent create/edit picker, Agents > Agent defaults, and Settings > Agents — blocking agent create/edit until the user clicked Install in Settings > Agents. ## Root cause One underlying bug, two victims: - **Boot false-negative.** The resolve cache is in-memory, so it starts cold on every launch. `resolve_command_cached` (the cheap path) consulted only the Buzz-managed shim dirs plus that cold cache, and `buzz_managed_command_path`'s allowlist structurally excludes `buzz-agent`. The bundled sidecar could therefore never resolve on the cheap path until a forced pass warmed the cache, so cheap-path surfaces rendered all-missing at boot. App setup never warms the cache. - **"Check again" hang.** `run_in_login_shell` used an untimeouted `Command::output()`; a wedged login shell froze the whole forced pipeline, leaving "Check again" spinning forever. ## What - `resolve_command_cached` now also calls `resolve_workspace_command`, resolving the bundled sidecar via a filesystem stat (no spawn) — the same class of work the managed-shim check already performs. `buzz-agent` can no longer report missing, even inside the boot warm window. - New `discovery/bounded_command.rs` runs any discovery child under a hard wall-clock deadline, polling with `try_wait` rather than blocking on `wait()`. Stdout and stderr are piped to two drain threads whose buffers share an aggregate `CAPTURE_LIMIT`; a breach fails closed (kill the tree, return `None`), so a noisy or hostile probe can force neither unbounded memory nor disk fill. Tree teardown runs on every exit path — timeout, error, cap breach, *and* success — because a login-shell rc file or auth CLI can legitimately background a descendant that would otherwise outlive discovery. Ownership is deliberately asymmetric: - **Unix:** the child leads its own process group (`process_group(0)`); teardown is `SIGTERM` → bounded grace → `SIGKILL` on the group. A descendant that leaves the group (`setsid`/`setpgid`) while holding a pipe is not owned and may survive one probe, but can never hang or unbound the helper: the Unix drains read nonblocking and end on `WouldBlock` once teardown sets the stop flag, so the join returns promptly without waiting on an escaped writer's EOF. - **Windows:** the child is spawned `CREATE_SUSPENDED`, assigned to a kill-on-close Job Object while frozen, then resumed. The job owns the root before any descendant can exist and is created without breakaway, so no writer can escape — a hard whole-tree guarantee, and closing the job reaps the tree even after the root has exited. Any failure to create, assign, or resume is fail-closed: the child is terminated and reaped and the spawn returns `None` (discovery treats it as command-not-found) rather than running unowned. - Each login-shell candidate is bounded by a 10s timeout via that helper, falling through to the next candidate on timeout instead of aborting the resolve. The login-shell path cache is generation-aware: a probe that loses to a concurrent refresh or lands mid-refresh returns the authoritative cached value (or re-probes under the new generation) rather than its own rejected local result, so a losing thread can never settle the UI with a PATH-missing catalog while the cache holds a fresh success. - Warm the ACP runtime catalog once at `AppShell` mount and gate the cheap-path surfaces on that pass. A module-level boot-warm state (`idle` → `pending` → `settled`/`failed`, deduped per launch) lets `useAcpRuntimesQuery` present a cold catalog as *loading* while the first forced pass runs and as a *retryable error* (carrying the probe's real reason) if it fails, instead of blessing "every harness not installed" as authoritative. A non-empty catalog always wins, so a revalidation or later failure never blanks a good list; the gate only overlays once the warm has started, so onboarding (which renders before the warm) is unaffected. Deduping per launch also fixes the previous per-remount re-fire. ## Verification Unix teardown and the drain contract are runtime-proven by `#[ignore]`-free tests that record a backgrounded descendant's real PID and assert the helper returns promptly on both the success and timeout paths without blocking on that writer. The generation-aware login-shell cache is covered by deterministic tests through a `cfg(test)` injectable probe seam that assert the function's return value under both concurrent-refresh interleavings — the losing caller returns the peer's committed success, and a mid-probe refresh forces a re-probe to the fresh value. The Windows ownership contract has no CI lane, so `bounded_command.rs` carries two `#[ignore]`-gated tests (spawn/assign race, looped; and the timeout path) for a sanctioned run on a Windows host. The boot-warm gate is covered by unit tests for the pure overlay and the `startBootWarm` failure → retry → settle lifecycle. Origin: [Buzz thread](buzz://message?channel=5ef5d5bb-643f-4b87-bbf4-e8b64585ffeb&id=a4b1c4485de4d35cff0f914d4f4211c796f44f670e76de2ef9431f7e882c906e) Fixes #6872 Related #6662 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
**Category:** improvement **User Impact:** Automatic mentions are easier to turn off and now behave consistently across conversations, settings, drafts, and repeated agent mentions. **Problem:** People found the new automatic mention behavior hard to control: turning it off in Settings did not reliably affect the composer, removing a mention could require also disabling the feature, and root/thread composers could inherit or restore surprising state. Other reported rough edges included only one of several mentioned agents becoming automatic, synthetic mentions leaking into drafts, restored mentions corrupting adjacent text, controls remaining visible in archived channels, and unclear picker feedback. See the [original feedback thread](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=c949ec399274fbb0d6633da3f95712e843a67dcada214f63d72f6975b406604b). **Solution:** Polish the existing feature around the problems people encountered, keeping automatic mentions controllable and scoped to the active conversation. | Reported issue | UX fix | | --- | --- | | Turning automatic mentions off in Settings did not reliably update the composer. | The global setting and composer control stay synchronized, and disabling the feature does not clear typed text. | | Removing an automatic mention could require both deleting the mention and turning off the feature. | Removing or unchecking an agent excludes that agent for the current conversation, while explicitly re-adding the agent can restore automatic mention behavior. | | Root and thread composers could share or restore surprising selections. | Each root or thread composer keeps its own automatic audience and restores it when the user returns. A request to enable automatic mentions only in agent threads was considered; this PR keeps them available at the channel root but prevents state from leaking between the two. | | Mentioning multiple agents could leave only one saved as automatic. | Multi-agent selections remain represented in the automatic audience and restored mention chips. | | Automatic mention prefixes could be saved as if the user typed them. | Synthetic prefixes stay out of persisted drafts while authored text is preserved. | | Restored mentions could lose their separator and corrupt continued typing. | Restored multi-word mentions retain their trailing space and place the caret after it. | | Archived channels showed automatic-mention state beside a disabled composer. | Disabled composers hide automatic-mention controls while preserving the draft and restoring state when re-enabled. | | Confirmation and picker behavior made the feature feel difficult to inspect or adjust. | Confirmations dismiss with removed agents, remain open while hovered, and expose the setting before it changes; pin icons, contrast, scope copy, animation, and keyboard toggling are also clarified. | | Agent suggestions and membership state could shift during directory refreshes. | Suggestions and membership labels stay stable during refreshes, while send-time authorization still revalidates access. | ## Changes <details> <summary>File changes</summary> **desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs** Adds coverage for the channel-roster eligibility rules used by agent mention autocomplete. **desktop/src/features/agents/lib/agentAutocompleteEligibility.ts** Aligns agent autocomplete eligibility with channel membership so available agents and their labels stay trustworthy. **desktop/src/features/channels/ui/MembersSidebar.tsx** Uses the shared member-pubkey logic when presenting and acting on channel members. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs** Covers preference changes that must remain stable while composer controls are toggled. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts** Keeps the automatic-mention preference as durable user intent rather than transient composer state. **desktop/src/features/messages/lib/mentionMemberPubkeys.ts** Centralizes which member identities count as mentionable in the current channel. **desktop/src/features/messages/lib/persistentAgentAudience.test.mjs** Expands lifecycle coverage for persistent agent audiences, explicit exclusions, and restored mentions. **desktop/src/features/messages/lib/persistentAgentAudience.ts** Models automatic, explicit, and excluded agent audiences separately so user choices survive updates without leaking across composers. **desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs** Verifies implicit automatic mentions are removed without damaging surrounding separators or authored content. **desktop/src/features/messages/lib/stripImplicitAgentMentions.ts** Strips presentation-only automatic mentions before draft persistence while preserving whitespace and authored text. **desktop/src/features/messages/lib/useMentions.ts** Routes mention insertion and removal through the composer-local audience lifecycle. **desktop/src/features/messages/lib/useRichTextEditor.ts** Preserves mention-chip structure and caret placement when automatic mentions are restored. **desktop/src/features/messages/ui/ComposerAddressControls.test.mjs** Updates control-state expectations for disabled automatic mentions and restored pin affordances. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** Makes automatic-mention state, disabled presentation, and pin controls visually explicit. **desktop/src/features/messages/ui/MentionAutocomplete.test.mjs** Adds coverage for roster labels, pin state, and picker behavior after mention selection. **desktop/src/features/messages/ui/MentionAutocomplete.tsx** Keeps the shortcut picker open for repeated selection and restores visible automatic-mention pin indicators. **desktop/src/features/messages/ui/MessageComposer.tsx** Scopes automatic mention state to each root or thread composer and coordinates restoration, draft persistence, and sending. **desktop/src/features/messages/ui/MessageComposerToolbar.tsx** Passes the effective automatic-mention state into the toolbar presentation. **desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs** Updates keyboard interaction coverage for toggling agents in place. **desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts** Restores automatic mention chips after lifecycle changes without moving or duplicating authored content. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Substantially expands coverage for toggles, exclusions, synchronization, and picker dismissal rules. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Keeps the picker usable across repeated choices and preserves explicit per-agent intent while settings change. **desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts** Makes the keyboard shortcut toggle the highlighted automatic audience choice without replacing unrelated selections. **desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts** Owns composer-local automatic-mention lifecycle behavior, including restoration, exclusions, deletion, and disabled-state handling. **desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs** Adds focused picker lifecycle coverage for selection, hover, and dismissal behavior. **desktop/src/features/messages/ui/useComposerMentionPicker.ts** Prevents premature picker dismissal while the user is interacting with its controls. **desktop/src/features/messages/ui/useDraftPersistSnapshot.ts** Persists only user-authored draft content rather than implicit automatic mention decorations. **desktop/src/shared/lib/keyboard-shortcuts.ts** Updates the automatic-mention shortcut description to match its toggle behavior. **desktop/src/testing/e2eBridge.ts** Extends the desktop test bridge with the state needed to exercise roster and automatic-mention transitions. **desktop/tests/e2e/mentions.spec.ts** Covers roster-based labels, managed-agent invitation, revocation, and recovery behavior in the complete mention flow. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Adds end-to-end coverage for root/thread isolation, preference synchronization, manual exclusions, draft hygiene, restored chips, separators, hover behavior, and disabled presentation. </details> ## Reproduction Steps 1. Open a channel with at least two available agents and enable automatic mentions from the composer mention control. 2. Select multiple agents, remove or uncheck one, and confirm subsequent composer updates keep that agent excluded while the others remain automatic. 3. Open a thread, choose a different automatic audience there, and switch between the thread and root composer; confirm each composer retains only its own choices. 4. Disable automatic mentions and confirm the draft text remains unchanged while automatic chips and controls show the disabled state; re-enable the setting and confirm eligible automatic chips return. 5. Delete an automatic mention chip, then explicitly add the agent again; confirm it immediately returns as an automatic mention without disturbing spaces or the caret, including for a multi-word name. 6. Reload with a saved draft and confirm implicit automatic mentions were not persisted as authored draft text. 7. Use the automatic-mention keyboard shortcut and picker repeatedly; confirm the picker remains open for additional choices and the highlighted agent toggles in place. ## Validation Validated at `34d208b47d64a9816f88e10a46bcfd479e917d75` after rebasing onto `origin/main` (`69096c9a8`): - Desktop unit tests: 5,731 passed, 0 failed. - Desktop TypeScript typecheck: passed. - Desktop E2E build: passed; emitted only existing chunk and dynamic-import warnings. - `pnpm check`: exited successfully; 4 warnings and 5 informational findings are in unrelated files introduced by current main. ## Screenshots/Demos The behavioral changes are covered by the focused desktop E2E scenarios above. Screenshots can be attached from the screenshot-producing automatic-mention E2E after the PR is created. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
## Summary A failed initial channel-history request no longer appears as an authoritative empty channel. The timeline now shows an announced error with a Retry action, while cached messages remain visible when a later refresh fails; successful empty channels continue to use their normal intro state. ### Related issue None found. ### Testing - Full desktop unit suite (`pnpm test`) - Desktop TypeScript check (`pnpm exec tsc --noEmit`) - Biome checks for changed files - Repository file-size ratchet - Full pre-push desktop checks and tests - Desktop app launched successfully against local Postgres and Redis for manual testing No screenshot is included because the new UI is only shown after a terminal relay-history failure; the regression test pins the error/empty/list precedence directly. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com>
…ivepack bypass The existing verify_push_rejects_receivepack_config test only verified the guard fires (returns Err with a receivepack message). It did not prove the bypass is executable — that without the guard, a custom remote.origin.receivepack actually causes ls-remote to read one destination while the real push lands at another. Add the positive-control block: seed remote with the first commit so ls-remote would exempt it, configure a wrapper script as receivepack that redirects all pushes to an empty redirect repo, then direct-push a second commit (bypassing verify_push). Assert the commit lands in redirect (not remote) — the split-service bypass is real. Also fix a cfg-gated unused-variable warning in kill_bounded_tree (child -> _child, silent on unix where it is unused, valid on non-unix where the #[cfg(not(unix))] branch calls _child.kill()). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) feat(mobile): push notifications MVP (#6269) refactor(db): extract domain stores from database runtime (#6987) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…dows Job Object teardown Addresses the three IMPORTANT blockers from Thufir's re-check against f406fb1. **Blocker 1 — remote_object_ids strict validation + 3 regressions** Change text.trim().is_empty() to text.is_empty(): whitespace-only ls-remote output is a malformed response, not an empty inventory. Add empty-refname rejection (tab at end with nothing after it) and extra-tab-field rejection (protocol variant the parser was not designed for). Both fail closed (None). Three regression tests exercise each surface with a fake git binary that emits the malformed output: - remote_object_ids_rejects_whitespace_only_output - remote_object_ids_rejects_empty_refname - remote_object_ids_rejects_extra_tab_fields **Blocker 2 — Windows process-tree teardown via Job Object** Replace CREATE_NEW_PROCESS_GROUP + Child::kill() with the suspended-child + kill-on-close Job Object lifecycle, mirroring the established pattern in desktop/src-tauri/src/managed_agents/discovery/bounded_command.rs. Child is spawned CREATE_SUSPENDED so no descendant can exist before the job owns the root (closes the spawn-to-assign race). BoundedJob wraps the HANDLE and drops it via CloseHandle on the kill path; JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ensures every descendant is reaped when the handle closes. kill_bounded_tree gains a #[cfg(windows)] job: &mut Option<BoundedJob> parameter (passed via inline #[cfg] at every call site) and drops the job rather than calling Child::kill(). Windows CI regression tests (reaps_backgrounded_descendant_on_windows, reaps_descendant_on_timeout_windows) are gated #[ignore] per the codebase convention for tests that require a Windows host; the test evidence demonstrates the Job Object lifecycle is materially identical to the bounded_command.rs reference. **Blocker 3 — crate-wide ENV_LOCK** Move the env-mutation lock from each test module's private static into a single pub static ENV_LOCK: Mutex<()> at the crate root (cfg(test)-gated). lib.rs mod tests and git_wrapper.rs mod tests both reference crate::ENV_LOCK, serializing all env mutations across one cargo-test process. lib.rs tests additionally restore the exact prior OsString value (via var_os before mutation, match on Some/None after) rather than blindly calling remove_var. This preserves whatever the surrounding test environment had set and avoids masking inter-test leaks. Unsafe blocks added throughout for set_var/remove_var per Rust 2024 edition. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses the two IMPORTANT blockers from Thufir's re-check against f4fc464. **Blocker 1 — four Clippy errors (Rust Lint + Windows Rust fail)** 1. manual_strip: replace `if key.starts_with("url.") { &key["url.".len()..] }` with `if let Some(subsection_and_rest) = key.strip_prefix("url.")`. 2. sliced_string_as_bytes: replace `subsection_and_rest[..sep].as_bytes()` with `&subsection_and_rest.as_bytes()[..sep]`. 3. redundant_guards: replace `k if k == "core.sshcommand"` with `"core.sshcommand"` (direct match arm pattern). 4. redundant_guards: replace `k if k == "core.gitproxy"` with `"core.gitproxy"`. All four are PR-introduced (absent from origin/main). **Blocker 2 — Windows regression tests + explicit CI step** Add two #[cfg(windows)] + #[ignore] tests for capture_raw_bounded: - capture_raw_bounded_reaps_descendant_on_success_windows: PowerShell root backgrounds a detached descendant (records its PID), waits for the PID file, exits 0. Asserts the descendant is dead after capture_raw_bounded returns Some. - capture_raw_bounded_reaps_descendant_on_timeout_windows: same setup but root loops forever, forcing the deadline. Asserts None returned and the descendant is dead. Both use windows_sys PROCESS_QUERY_LIMITED_INFORMATION + GetExitCodeProcess to prove the descendant PID is gone, not just assumed reaped. Mutation: removing BoundedJob or making kill_bounded_tree a no-op leaves the descendant alive and the assert fails. Add helpers windows_pid_alive(u32) -> bool and read_windows_pid(&str) -> u32 for PID probing and asynchronous PID-file reading. Add a "Test (buzz-git-identity)" CI step to the Windows Rust job in ci.yml, placed after the existing "Test (buzz-dev-mcp)" step. The step runs `cargo test -p buzz-git-identity --target $TARGET -- --run-ignored --test-threads=1`, so the #[ignore]-tagged tests actually execute in CI rather than being dead-letter. `--test-threads=1` serializes the PowerShell launches to keep wall-clock predictable on the runner. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
All 9 are in test-only code introduced in the previous commit; no production logic touched. 8x needless_borrows_for_generic_args: drop the & on .args(&[...]) array literals in eight Command::args call sites (the array literal coerces to a slice without the borrow). 1x manual_contains (line 5870): replace caps_keys.iter().any(|k| *k == "core.sshcommand") with caps_keys.contains(&"core.sshcommand") Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… step --run-ignored is a cargo-nextest flag; the libtest harness used by cargo test does not recognise it. Replace with --ignored (libtest's flag for running only #[ignore]-tagged tests) so the two Windows reap-regression tests actually execute on the windows-latest runner. Also update the matching comment in ci.yml and the #[ignore = "..."] message strings in git_wrapper.rs to reference --ignored. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Problem
Agent sessions could commit under the human operator's ambient git identity. Only
buzz-dev-mcp's shim applied the nostr author/signingGIT_CONFIG_*env, and only to its own shell-tool children — the native shells of claude-code, codex, and goose never saw it. A baregit commitin those shells resolved to whateveruser.name/user.emailthe repo or global config carried, erasing the AI-attribution signal (e.g. #3140, which landed under a human identity).Change
Makes the agent commit author identity machine-managed and deterministic across every harness. Trailers still credit the human operator (
Co-authored-by+Signed-off-by); that policy is unchanged.buzz-git-identity(new crate) — the single source of truth for the author/email, NIP-GS signing, and keyfile logic, consumed by both the dev-mcp shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. The identity email is<64-hex-pubkey>@<relay-host>; the author name is the sanitized Buzz display name (falls back to thenpub).0600identity manifest. The enforcement wrapper locates its own install directory by canonicalizing thegitonPATHagainst its own executable — the same trust channel it uses to find the realgit, immune to environment rewriting — reads the expected identity from the manifest, and injects it directly as the highest-precedence config, never trustinggit config user.email. A manifest that is missing or fails to parse in an otherwise-managed session fails the invocation closed rather than silently dropping enforcement. Manifest present = enforce; genuinely absent (keyless/unconfigured session) = passthrough.gitenforcement wrapper (git_wrapper.rs) — installed onPATHahead of the real binary.-cargs at the front of the resolved command, so it outranks caller-supplied-c,GIT_CONFIG_PARAMETERS,include.path, and repo/global config alike. ScrubsGIT_AUTHOR_*/GIT_COMMITTER_*; rejects-c user.*,--config-env=user.*,--author, and--reset-author(scoped tocommit/amsogit log --authorstill works).commit -C/-c <sha>and--amendcreate new commits that reuse another commit's author; injected config cannot override a reused author, so the wrapper inspects the resulting author and rejects any commit-creating mode that would leave a non-agent author. Ordinary commits and amends of the agent's own commits pass.git push --dry-run --porcelain --no-verify <original args>to obtain the exact resolved update set — covering config-defined and inline-c alias.*git aliases,-C/--git-dir,--all/--mirror/--tags, configremote.*.push, and wildcard refspecs — then refuses any outgoing commit not authored by the agent identity, naming the offending sha and email. A human-authored commit is exempted only when it is a patch-id-identical replay of a commit already upstream (a legitimately cherry-picked/rebased human commit — correct attribution, not new agent work masquerading as someone else); any other non-agent author is refused. The dry-run probe is bounded by a hard timeout; a timeout, an unresolvable update set, or any dry-run failure fails closed. The--no-verifyon the internal dry-run keeps it from double-running the repo's own pre-push hooks; the real push keeps its hooks, and enforcement runs unconditionally so--no-verifyon the real push cannot bypass it.-coptions, so an alias could otherwise plant higher-precedence config that re-authors or unsigns the commit — and git's quote-aware alias parser sees tokens differently from a naive whitespace scan ('-c' 'user.email=…'dequotes to real config). Rather than model that grammar, the wrapper admits a non-shell alias only when every token of its resolved body is a trivially-safe bare word: no quote or backslash characters, no-c/--config-envchannel, and no=-valued option. Anything else is refused. Shell (!) aliases are refused outright in a managed session — git runs their body with the real git ahead of the wrapper onPATH, so an inner-coutranks the inherited authority and can commit or push under an arbitrary identity, unsigned; there is no safe subset to allow. A bare-word alias can still carry identity/signing flags (commit --author … --no-gpg-sign), so on success the alias is expanded and its resolved command — accumulated body tokens across up to ten alias substitutions plus the caller's trailing argv; if another alias remains at that bound, the wrapper refuses rather than treating a partial expansion as resolved — is held to the identical identity/signing policy as the same command typed directly (enforce()and the commit-author preflight, keyed on the expanded subcommand). An alias can therefore never do more than its expansion could typed directly, and there is no alias-specific flag list to keep in sync. Bare-word aliases whose expansion is clean keep working (alias.ci = commit,alias.st = status,alias.lg = log --oneline,alias.pub = push origin main).-c commit.gpgSign=falseand--no-gpg-signare rejected at argv; env-based signing-disable is defeated by the injected highest-precedence config.buzz-acp) —AcpClient::spawnwrites the keyfile and manifest, installs the wrapper plus the nostr signer/credential helpers viabuzz-acp's own multicall personalities, prepends the wrapper dir to the childPATH, and applies the identity + signingGIT_CONFIG_*composed over the desktop's per-URL credential helper. The key is sourcedBUZZ_PRIVATE_KEY(the documented required secret) beforeNOSTR_PRIVATE_KEYat both the command and process-env layers, and the canonical key is restaged unconditionally as the child'sNOSTR_PRIVATE_KEYso the harness and dev-mcp shim can never install split identities. A managed session fails closed when deterministic identity cannot be installed; sessions with no nostr key are skipped entirely, so test spawns and unconfigured sessions are unchanged. Unix-only.base_prompt.md,nest_agents.md) — identity is machine-managed; credit the operator viaCo-authored-by/Signed-off-bytrailers, neveruser.name/user.email/-c/--author.BUZZ_GIT_IDENTITY=agent|user, defaultagent, settable per-agent via persona env).agentis everything above.userreuses the existing review-hardened unconfigured-session path — the harness installs no wrapper onPATH, no manifest, no keyfile, and no injected authorship/signing config, so vanilla git resolves the operator's own repo/global identity and signing. This matchesVISION_SOVEREIGN.md: the operator, not the platform, decides whose identity their agent's commits carry on their own machine. The nostr credential helper (relay git-over-HTTP auth) keeps installing inusermode, and the harness still stages the canonical key as the child'sNOSTR_PRIVATE_KEYbefore itsuser-mode early return so the helper can authenticate on every launch path (headless as well as desktop) — auth is not attribution, and settinguseron a configured session must not silently drop relay git auth. The mode is read once at spawn by the harness and dev-mcp shim, never by the wrapper per-invocation, so an agent cannotexport BUZZ_GIT_IDENTITY=usermid-session to disable enforcement. An unrecognized value fails the spawn loudly, naming the variable and its two values, rather than silently choosing a mode.usermode drops the operator out of commit-level AI attribution — deliberate, their claim to make; in that mode theCo-authored-by/Signed-off-bytrailers are redundant since the commit already is the operator's identity.Scope and ceiling
This is a deterministic best-effort local control, not an adversarial sandbox. Enforcement is a
PATHwrapper sharing the OS user with the agent, so it closes accidental identity leakage — the entire class behind #3140 — but does not stop a deliberate bypass: invoking/usr/bin/gitby absolute path,env -i, replacing the wrapper onPATH, deleting the manifest, or committing via libgit2/jj all sidestep it by design. A hard guarantee that holds regardless of what the agent runs would require a receive-side gate on the git host; that remains a possible separate follow-up. This PR intentionally targets the default-path commit/push surface, which is where the missed-attribution signal originates.Verification
Beyond unit tests (
buzz-git-identity66,buzz-acp817 unit + integration suites), the wrapper mechanism is exercised end-to-end by process-level tests (git_identity_enforcement.rs) that spawn the realbuzz-acp-as-gitmulticall against a real repo with a manifest present:-c user.email=…,--author=…) is rejected;<display-name> <hex@relay>;alias.quoted = '-c' 'user.email=…' commit) and a shell (!) commit alias are each refused before git runs and leaveHEADunchanged, while a plain-subcommand alias still resolves and commits agent-authored; a bare-word alias carrying identity/signing flags (alias.human = commit --author … --no-gpg-sign,--no-gpg-signalone, and a two-hop chain) is expanded and refused by the same policy as the typed command,HEADunchanged; a chain of exactly ten aliases reachingcommitremains usable, while an eleventh alias is refused before git runs with unbornHEADand zero commit objects;Each process-level test is mutation-verified: nulling the harness's
install_git_identitywiring, or dropping theenforce/verify_pushdispatch, turns the corresponding test red — confirming the layer is wired into the process boundary, not merely unit-covered. The key-precedence and bounded-probe paths carry their own mutation-sensitive unit tests. The keyfile-lifecycle test (keyfile_lifecycle.rs) spawns the real binary on its error-exit path and confirms the0600keyfile is deleted on every exit.The
BUZZ_GIT_IDENTITYtoggle is covered at three layers:GitIdentityModeparsing unit tests (unset →agent, exactagent/userwith whitespace tolerance, and loud rejection of typos/case-variants/empty naming the variable and both values); harness gate tests (userinstalls no identity dir orGIT_CONFIG_*, a persona-staged value outranks process env, explicitagentstill writes the manifest, an invalid value errors at spawn); and shim seam tests (agentinstalls thegitwrapper + signer + manifest + authorship config;userkeeps only thegit-credential-nostrhelper and itsnostr.keyfilepointer with no wrapper, signer, manifest, or authorship/signing config; unset defaults toagent; invalid fails install).Notes
Co-authored-bytrailer email.git-sign-nostr.